/*
* Extension to array
*/
Array.implement({
    /*
    * Collection of utilities for array of objects management
    */

    indexOf: function(item) {
        /*
        * Find first occurence of item in array
        * Optional: search for item with key==value
        */
        var ret=-1;
        this.some(function(el, idx) {
            var equal=false;
            if (typeOf(item) == 'object') {
                Object.keys(item).forEach(function(key) {
                    equal = (item[key]==el[key]);
                });
            } else {
                equal = (item == el);
            }
            if (equal) {
                ret=idx;
            }
        });
        return ret;
    },
    find: function(item) {
        /*
        * Return first object that is "equal" to item, i.e. has all properties with same value
        */
        idx = this.indexOf(item);
        if(idx>-1) {
            return this[idx];
        } else {
            return undefined;
        }
    },
    getItem: function(key, value) {
        /*
        /* Get first item with key == value, if content are objects
        */
        matches=this.filter(function(el, idx) {
            if (typeOf(el) == 'object') {
                return (el[key] == value);
            } else {
                return (el == key);
            }
        });
        return matches[0];
    },
    combineObjs: function(objList) {
        /*
        * Merge a new array into this one, skipping duplicates
        */
        objList.forEach(function(el) {
            if(this.indexOf(el)<0) { // item is not present in array
                this.include(el);
            }
        }.bind(this));
    }
});

/*
* Utility
*/
function fromJSON(object, debug) {
    // If item is a string decode it as JSON, return object
    if (typeOf(object) == 'string') {
        if (debug) console.debug("JSON decode of '" + object + "'");
        return JSON.decode(object);
    } else {
        if (debug) console.debug("No JSON decode necessary for " , object);
        return object;
    }
};
function toJSON(object, debug) {
    // If item is a string decode it as JSON, return object
    if (typeOf(object) != 'string') {
        if (debug) console.debug("JSON encode of ",object);
        return JSON.encode(object);
    } else {
        if (debug) console.debug("No JSON encode necessary for " + object);
        return object;
    }
};

/*
*
* dataStore either static or via JSON - DS
*
* Basically it is an array of key->value items.
* Actually you must define key and value (default to 'id' and 'label') that are
* used for comparison and existence check, but items can have other attributes
* too
*
*  Tree:
*     dataStore (static items, no add, no mod)
*     |_ dataStoreAjax (extend dataStore with AJAX management of items)
*        |_ dataStoreREST (extend dataStoreAjax with RESTful URLs)
*
*  methods:
*   get: return one item by id
*   filter: return list of items using a /mask/ regexp
*   add: create a new object
*   mod: modify an existing object
*   del: delete an existing object
*
*
*/

var dataStore = new Class({
    Implements: [Options, Events],
    debug: true,
    items: [],
    options: {
        key: 'id',
        value: 'label',
        items: [] // can be an array of objects or a JSON string
    },

    initialize: function(options) {
        this.setOptions(options);
        // Decode items if necessary, else copy them
        this.items=fromJSON(this.options.items, this.debug);
        this.key = this.options.key;
        this.value = this.options.value;
        this._debug();
    },
    equal: function(item1, item2){
        // Basic equality of two items
        var equal=false;
        if(typeOf(item1) == 'object') {
            Object.keys(item1).some(function(key) {
                equal= (item1[key] == item2[key]);
            })
        } else {
            equal= (item1 == item2);
        }
        return equal;
    },
    sort: function(key) {
        if(this.items.length) { // Do not sort empty array
            if(typeOf(this.items[0]) == 'object') {
                this.items.sort(function(a,b) {
                    return a[key] > b[key]
                });
            } else {
                this.items.sort(function(a,b) {
                    return a > b;
                });
            }
        }
        return this.items;
    },
    _debug: function() {
        if (this.debug) console.debug(this.items);
    },

    // Standard methods, interfaces
    //
    list: function(mask) {
        // Return all items, eventually filtered by /mask/
        items=this._list(mask);
        this.fireEvent('list', items);
        return items;
    },
    get: function(id) {
        // Get item with this.options.key == id
        console.debug('get');
        item=this._get(id);
        this.fireEvent('get', item);
        return item;
    },
    add: function(item) {
        var newItem;

        // Clean up params and call _add
        item=fromJSON(item, this.debug);
        // Check for duplicates
        if(this.items.find(item)) {
            console.debug("Item already present, skipping...");
            newItem = item;
        } else {
            newItem = this._add(item);
        }
        this._debug();
        this.fireEvent('add', newItem);
        return newItem;
    },
    mod: function(item) {
        // Modify an item by its id,
        item=fromJSON(item, this.debug);
        // Check for existence
        var index;
        index = this.items.indexOf(this.get(item.id));
        if (index>-1) {
            this._mod(index, item);
            this._debug();
        } else {
            if(this.debug) {
                console.debug("Item not  present, cannot modify...");
            }
            item=null;
        }
        this.fireEvent('mod', item);
        return item;

    },
    del: function(item) {
        // Delete an item from items
        var delItem;

        item=fromJSON(item, this.debug);
        if(this.items.find(item)) {
            delItem=this._del(item);
        } else {
            console.debug("Item not present, cannot delete...");
            delItem=null;
        }
        this.fireEvent('del', delItem);
        return delItem;
    },
    //
    // *******************************************
    // Basic methods to be overriden when deriving
    // *******************************************
    //
    _list: function(expr) {
        if(expr) {
            return this.items.filter(function(el, idx) {
                return el[this.options.value].contains(expr)
            },
            this);
        } else {
            return this.items;
        }
    },
    _get: function(id) {
        return this.items.getItem(this.key, id);
    },
    _add: function(item) {
        // Real add function, override when deriving
        this.items.push(item);
        return item;
    },
    _mod: function(index, item) {
        // Real mod function, override when deriving
        // params are:
        //  - index: index of object we will modify
        //  - item: modified object
        this.items[index] = item;
    },
    _del: function(item) {
        this.items.erase(item);
    },

    _sortFunction: function(a, b) {
        // Default sorting method, override if necessary
        return (a[this.key] > b[this.key]);
    },

});

/*
* Extends dataStore to use AJAX call for managing items
*
* Options are:
*  - url: base url, to which we will attach action and param
*  - lazy: be lazy, load items only when necessary
*  - readonly: cannot create or mod items
*
* AJAX Protocol
* - action is one of:
*    get: get one item by id, param is id
*    list: get items by expr (default ALL), param is filtering expression; not anchored match, i.e.: /expr/
*    add: create new item, param is item (JSON encode of item)
*    mod: modify an existing item, param is item (JSON encode of item)
*    del: delete an existing item, param is item (JSON encode of item)
*
* function     | URL
* ---------------------------------
* list         | ?action=list&param=<expr> (optional parameter, omit for all)
* get          | ?action=get&param=<jsondumpofobject>
* add          | ?action=add&param=<jsondumpofobject>
* mod          | ?action=mod&param=<jsondumpofobject>
* del          | ?action=del&param=<jsondumpofobject>
* TODO
* Cleanup lazy part to reduce calls, equality of items is essential
*/
var dataStoreAjax = new Class({
    Extends: dataStore,
    Implements: [Options, Events],
    debug: true,
    isRest: false,
    isQS: true,
    items: [],
    options: {
        lazy: false,
        async: false,
        readonly: false,
        url: undefined,
        items: [], // can be an array of objects or a JSON string
    },

    initialize: function(options) {
        this.parent(options);
        this.setOptions(options);
        if (this.options.url) {
            this.url = this.options.url; // http://localhost:5000/stResponder?cls=S or http://localhost:5000/stResponder/S
        }
        if(!this.options.lazy){
            this._list(); // If not lazy load everything
        }
    },

    // URL management and AJAX Loading
    buildURL: function(action, param) {
        var url;
        url = this.url.toURI();
        url.setData('action', action);
        if (param) {
            url.setData('param', param);
        }
        if (this.debug) {
            console.debug('URL: ', url.toString());
        }
        return url.toString();
    },
    buildReq: function(action, param) {
        var url = this.buildURL(action, param);
        var req = new Request.JSON({
            url: url,
            method:'get',
            async: this.options.async
        });
        return req;
    },
    // Load from URL
    __load: function() {
        req = this.buildReq('list');
        req.onSuccess = function(json, text) {
            items = fromJSON(json, this.debug);
            this.items=items;
        }.bind(this);
        req.send()
        return this.list();
    },
    _list: function(expr) {
        if(this.options.lazy) {
            // Request other values and merge
            req = this.buildReq('list', expr);
            req.onSuccess = function(json, text) {
                items = fromJSON(json, this.debug);
                this.items.combineObjs(items);
            }.bind(this);
            req.send();
        }
        return this.parent(expr);
    },
    _get: function(id) {
        if(this.options.lazy) {
            // Request value and merge with items
            req = this.buildReq('get', id);
            req.onSuccess = function(json, text) {
                // item = fromJSON(json, this.debug);
                this.items.combineObjs([json]);
            }.bind(this);
            req.send();
        }
        return this.parent(id);
    },
    _add: function(object) {
        /*
        * If it is possible (not readonly)
        * try to add a new item.
        * return:
        * - the added item, on success
        * - an error object, on failure (obj.isError == true)
        */
        if (this.options.readonly) {
            console.log('Readonly dataStore, cannot add Item');
            return null;
        }
        var index = this.items.indexOf(object);
        if(index>-1) {
            console.debug('Item already present');
            return this.items[index];
        }

        var newItem;
        req = this.buildReq('add', JSON.encode(object));
        req.onSuccess = function(json, text) {
            newItem=json;
            this.items.combineObjs([newItem]);
        }.bind(this);
        req.onFailure = function(xhr) {
            newItem = fromJSON(this.xhr.responseText, this.debug);
            newItem.isError=true;
        };
        req.send();

        return newItem;
    },
    _mod: function(index, item) {
        /*
        * If it is possible (not readonly)
        * try to mod an item.
        * return:
        * - the modded item, on success
        * - an error object, on failure (obj.isError == true)
        */

        if (this.options.readonly) {
            console.log('Readonly dataStore, cannot mod Item');
            return null;
        }

        var newItem;
        req = this.buildReq('mod', JSON.encode(item));
        req.onSuccess = function(json, text) {
            newItem = json;
            this.items[index]=json;
        }.bind(this);
        req.onFailure = function(xhr) {
            newItem = fromJSON(this.xhr.responseText, this.debug);
            newItem.isError=true;
        };
        req.send();
        return newItem;
    },
    _del: function(item) {

        if (this.options.readonly) {
            console.log('Readonly dataStore, cannot del Item');
            return null;
        }

        var newItem;
        req = this.buildReq('del', JSON.encode(item));
        req.onSuccess = function(json, text) {
            newItem=item;
            this.parent(item); // Remove from items array
        }.bind(this);
        req.onFailure = function(xhr) {
            newItem = fromJSON(this.xhr.responseText, this.debug);
            newItem.isError=true;
        };
        req.send();
        return newItem;
    }
});

/*
* Extends dataStoreAJAX to use a RESTful URL schema
*
* URL schema is following:
*
* given a DS like ds = new dataStoreAjax({url:'/baseURL/part1'})
*
* function     | URL               | payload                  | method
* --------------------------------------------------------------------
* list         | /baseURL/part1    | mask=<filter> (optional) | GET
* get          | /baseURL/part1/id |                          | GET
* add          | /baseURL/part1    | item=<jsondumpofobject>  | POST
* mod          | /baseURL/part1/id | item=<jsondumpofobject>  | PUT
* del          | /baseURL/part1/id |                          | DELETE
*
* TODO
* Not fully RESTful compliant, must change URL building and method calling, for example
*  - list: /baseURL/part1 and method GET
*  - get: /baseURL/part1/id and method GET
*  - add: /baseURL/part1 and method POST
*  - mod: /baseURL/part1/id and method PUT
*  - del: /baseURL/part1/id and method DELETE
*
*/

var dataStoreREST = new Class({
    Extends: dataStoreAjax,
    Implements: [Options, Events],
    debug: true,
    items: [],
    isRest: true,
    isQS: false,
    options: {
        lazy: false,
        async: false,
        readonly: false,
        url: undefined,
        items: [], // can be an array of objects or a JSON string
    },

    initialize: function(options) {
        this.parent(options);
        this.setOptions(options);
        if (this.options.url) {
            this.url = this.options.url; // http://localhost:5000/stResponder?cls=S or http://localhost:5000/stResponder/S
        }
        if(!this.options.lazy){
            this.__load();
        }
    },

    buildReq: function(action, param) {
        var url;
        var payload; // Payload
        var method; // Method (GET/POST/PUT/DELETE)

        param = fromJSON(param);
        console.debug('action:'+action+' param:'+param);
        switch (action) {
            case "list":
                url = this.url;
                if (param) {
                    payload={'mask': param};
                }
                method='get';
            break;

            case "get": // We pass a new Item, without id
                url = this.url+'/'+param.id;
                method='get';
            break;

            case "add": // We pass a new Item, without id
                url = this.url;
                payload = {'item': toJSON(param)};
                method='post';
            break;

            case "mod": // We pass a modified Item, with same ID
                url = this.url+'/'+param.id;
                payload = {'item': toJSON(param)};
                method='put';
            break;

            case "del": // We pass the item we want to remove
                url = this.url+'/'+param.id;
                method='delete';
            break;
        }
        console.debug('url '+url+' pl '+payload+' method: '+method);
        req = new Request.JSON({
            url: url,
            method: method,
            data: payload,
            async: false});
        console.debug(req);
        return req;
    },
});

//
// Select with DS
// <select classs="selectDS" name="xxx" ds:id="id" ds:html="label">
//
var selectDS = new Class({
    Implements: [Options, Events],
    debug: false,
    options: {
        ds: undefined,
        id: 'id',
        html: 'label'
    },

    initialize: function(target, options) {
        this.setOptions(options);
        this.target = target;

        this.ds = this.options.ds
        this.id = this.target.hasAttribute('ds:id') ? this.target.get('ds:id') : this.options.id;
        this.html = this.target.hasAttribute('ds:html') ? this.target.get('ds:html') : this.options.html;
        this.value = this.target.hasAttribute('ds:value') ? this.target.get('ds:value') : undefined;
        this.fillOpt(this.value);
    },
    fillOpt: function(selectedVal) {
        // Fill options
        if (this.ds && this.ds.items.length) {
            Array.forEach(this.ds.items, function(el) {
                var option = new Element('option', {
                    'html': el[this.html],
                    'value': el[this.id]
                });
                if (el[this.id] == selectedVal) option.setAttribute('selected', 'selected');
                option.inject(this.target);
            }.bind(this));
        }
    },
    refresh: function() {
        this.target.getChildren('option').destroy();
        this.ds.refresh();
        this.fillOpt(this.value);
    }

});

function activeSelectDS(objectList, options) {
    if (!options) options = {};
    objectList.forEach(function(el) {
        new selectDS(el, options);
    });
}
/* ---------------------------------------------- */

//
// Numeric formatted input
// Formats a number with 'decimal' places in current locale
//
// Validator
//
Form.Validator.add('validate-formatted-number', {
    errorMsg: 'Please, enter a number',
    test: function(el, props) {
        var val = el.get('value');
        var gsep = Locale.get('Number').group.escapeRegExp();
        var returnValue = val.replace(new RegExp(gsep, 'g'), '').replace(Locale.get('Number').decimal, '.');
        return (!isNaN(Number(returnValue)));
    }
});

//
// Widgets
//
//
// Generic Numeric, decimals defaults to 0
//
var numericInput = new Class({
    Implements: [Options, Events],
    formattedElement: null,
    debug: true,
    options: {
        decimals: 0
    },

    initialize: function(target, options) {
        this.setOptions(options);
        this.target = target;

        var value = this.target.get('value');
        var formatted = parseFloat(value);

        this.formattedElement = this.target.clone();
        this.formattedElement.addClass('numeric');
        this.formattedElement.addClass('numericInput');
        this.formattedElement.addClass('validate-formatted-number');
        // this.formattedElement.set('style', 'width:120px;');

        // Events
        this.formattedElement.addEvent('change', function() {;
            this.copyValues()
        }.bind(this));

        this.target.addEvent('change', function() {
            this.copyValuesBG()
        }.bind(this));

        this.formattedElement.setAttribute('value', formatted.format({
            decimals: this.options.decimals
        }));

        this.target.hide();
        this.debug ? this.formattedElement.set('title', this.target.get('value')) : null;
        this.formattedElement.removeAttribute('name'); // Delete name, so it is not passed from form

        this.formattedElement.inject(this.target, 'after');

    },
    setValue: function(value, clean) {
        this.formattedElement.set('value', value.format({
            decimals: this.options.decimals
        }));
        this.target.set('value', value);
        this.debug ? this.formattedElement.set('title', this.target.get('value')) : null;
    },
    cleanUp: function(num) {
        var gsep = Locale.get('Number').group.escapeRegExp();
        console.debug('num',num,'gsep',gsep);
        var returnValue = num.replace(new RegExp(gsep, 'g'), '').replace(Locale.get('Number').decimal, '.');
        console.debug(returnValue);
        return Number(returnValue);
    },
    copyValues: function() {
        var value = this.formattedElement.get('value');
        value = this.cleanUp(value);
        this.setValue(value);
    },
    copyValuesBG: function() {
        var value = this.target.get('value');
        this.setValue(value);
    },

});

// Shortcut for metric tons (3 decimal places)
var tonsInput = new Class({
    Extends: numericInput,
    options: {
        decimals: 3
    },
});

// Shortcut for decimal time (5 decimal places)
var timeInput = new Class({
    Extends: numericInput,
    options: {
        decimals: 5
    },
});

// Shortcut for money amount (2 decimal places)
var moneyInput = new Class({
    Extends: numericInput,
    options: {
        decimals: 2
    },
});

// Activators
function activeTonsInput(objectList, options) {
    objectList.forEach(function(el) {
        new tonsInput(el, options);
    });
}

function activeTimeInput(objectList, options) {
    objectList.forEach(function(el, options) {
        new timeInput(el, options);
    }.bind(this));
}

function activeMoneyInput(objectList, options) {
    objectList.forEach(function(el, options) {
        new moneyInput(el, options);
    }.bind(this));
}

/* ---------------------------------------- */

//
// MONEY WIDGET
// IN:  input id={ID} value=XXXX.XX,Y
// OUT: change target value
//
var widgetMoney = new Class({
    Implements: [Options, Events],
    amountElement: null,
    currencyElement: null,
    debug: true,
    options: {
        currencies: undefined,
        decimals: 2,
        currency: 'select' // plain - print symbol, select - create select, none - do not print
    },

    initialize: function(target, options) {
        this.setOptions(options);
        this.target = target;
        this.currencies = this.options.currencies;

        var value = this.target.get('value').split(',');
        var amount = parseFloat(value[0]);
        var currency_id = parseInt(value[1]);

        this.amountObj = new moneyInput(this.target);

        this.currencyObj = new selectDS(new Element('select', {
            style: 'width:70px; margin-left:5px;',
            'ds:value': currency_id,
            'disabled': this.target.getProperty('disabled') || this.target.getProperty('readonly'),

        }), {
            ds: this.currencies,
            html: 'html'
        });
        this.currencyObj.target.inject(this.amountObj.formattedElement, 'after');

        // Events management
        var copyVals = function() {
            this.copyValues();
        }.bind(this);

        this.amountObj.formattedElement.removeEvents('change');
        this.amountObj.formattedElement.addEvent('change', copyVals);
        this.currencyObj.target.addEvent('change', copyVals);

    },
    copyValues: function() {
        var amount = this.amountObj.formattedElement.get('value');
        amount = this.amountObj.cleanUp(amount);

        var currency_id = this.currencyObj.target.get('value');
        var complexValue = amount + ',' + currency_id;

        this.target.set('value', complexValue);
        this.amountObj.formattedElement.set('title', complexValue);
        this.amountObj.formattedElement.set('value', amount.format({
            decimals: this.options.decimals
        }));

    }
});

function activeWidgetMoney(objectList, options) {
    currencies = [];
    if (!options) options = {};
    objectList.forEach(function(el) {
        new widgetMoney(el, options);
    });
}

